/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.cxf.jaxrs.microprofile.openapi;

import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.ws.rs.core.Application;

import org.apache.cxf.Bus;
import org.apache.cxf.annotations.Provider;
import org.apache.cxf.annotations.Provider.Scope;
import org.apache.cxf.annotations.Provider.Type;
import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.endpoint.Server;
import org.apache.cxf.feature.AbstractFeature;
import org.apache.cxf.jaxrs.JAXRSServiceFactoryBean;
import org.apache.cxf.jaxrs.common.openapi.DefaultApplicationFactory;
import org.apache.cxf.jaxrs.common.openapi.SwaggerProperties;
import org.apache.cxf.jaxrs.model.AbstractResourceInfo;
import org.apache.cxf.jaxrs.provider.ServerProviderFactory;
import org.apache.cxf.jaxrs.swagger.ui.SwaggerUiConfig;
import org.apache.cxf.jaxrs.swagger.ui.SwaggerUiSupport;
import org.apache.geronimo.microprofile.openapi.config.GeronimoOpenAPIConfig;
import org.apache.geronimo.microprofile.openapi.impl.model.ContactImpl;
import org.apache.geronimo.microprofile.openapi.impl.model.InfoImpl;
import org.apache.geronimo.microprofile.openapi.impl.model.LicenseImpl;
import org.apache.geronimo.microprofile.openapi.impl.model.OpenAPIImpl;
import org.apache.geronimo.microprofile.openapi.impl.processor.AnnotationProcessor;
import org.apache.geronimo.microprofile.openapi.impl.processor.reflect.ClassElement;
import org.apache.geronimo.microprofile.openapi.impl.processor.reflect.MethodElement;
import org.apache.geronimo.microprofile.openapi.impl.processor.spi.NamingStrategy;
import org.eclipse.microprofile.openapi.models.OpenAPI;


@Provider(value = Type.Feature, scope = Scope.Server)
public class OpenApiFeature extends AbstractFeature implements SwaggerUiSupport, SwaggerProperties {
    private static final Logger LOG = LogUtils.getL7dLogger(OpenApiFeature.class);

    private String version;
    private String title;
    private String description;
    private String contactName;
    private String contactEmail;
    private String contactUrl;
    private String license;
    private String licenseUrl;
    private String termsOfServiceUrl;
    // Read all operations also with no @Operation
    private boolean readAllResources = true; 
    // Scan all JAX-RS resources automatically
    private boolean scan = true;
    private boolean prettyPrint = true;
    private boolean runAsFilter;
    private Collection<String> ignoredRoutes;
    private Set<String> resourcePackages;
    private Set<String> resourceClasses;
    private String filterClass;

    private Boolean supportSwaggerUi;
    private String swaggerUiVersion;
    private String swaggerUiMavenGroupAndArtifact;
    private Map<String, String> swaggerUiMediaTypes;

    // Allows to pass the configuration location, usually openapi-configuration.json
    // or openapi-configuration.yml file.
    private String configLocation;
    // Allows to pass the properties location, by default swagger.properties
    private String propertiesLocation = DEFAULT_PROPS_LOCATION;
    // Allows to disable automatic scan of known configuration locations (enabled by default)
    private boolean scanKnownConfigLocations = true;
    // Swagger UI configuration parameters (to be passed as query string).
    private SwaggerUiConfig swaggerUiConfig;

    @Override
    public void initialize(Server server, Bus bus) {
        final JAXRSServiceFactoryBean sfb = (JAXRSServiceFactoryBean)server
            .getEndpoint()
            .get(JAXRSServiceFactoryBean.class.getName());

        final ServerProviderFactory factory = (ServerProviderFactory)server
            .getEndpoint()
            .get(ServerProviderFactory.class.getName());

        final Set<String> packages = new HashSet<>();
        if (resourcePackages != null) {
            packages.addAll(resourcePackages);
        }

        final Application application = DefaultApplicationFactory.createApplicationOrDefault(server, factory, 
            sfb, bus, resourcePackages, isScan());

        final AnnotationProcessor processor = new AnnotationProcessor(GeronimoOpenAPIConfig.create(),
            new NamingStrategy.Http(), null /* default JsonReaderFactory */);

        final OpenAPIImpl api = new OpenAPIImpl();

        if (isScan()) {
            packages.addAll(scanResourcePackages(sfb));
        }
        
        final Set<Class<?>> resources = new HashSet<>();
        if (application != null) {
            processor.processApplication(api, new ClassElement(application.getClass()));
            LOG.fine("Processed application " + application);
            
            if (application.getClasses() != null) {
                resources.addAll(application.getClasses());
            }
        } 

        resources.addAll(sfb
            .getClassResourceInfo()
            .stream()
            .map(AbstractResourceInfo::getServiceClass)
            .filter(cls -> filterByPackage(cls, packages))
            .filter(cls -> filterByClassName(cls, resourceClasses))
            .collect(Collectors.toSet()));
        
        if (!resources.isEmpty()) {
            final String binding = (application == null) ? "" 
                : processor.getApplicationBinding(application.getClass());
            
            resources
                .stream()
                .peek(c -> LOG.info("Processing class " + c.getName()))
                .forEach(c -> processor.processClass(binding, api, new ClassElement(c),
                    Stream.of(c.getMethods()).map(MethodElement::new)));
        } else {
            LOG.warning("No resource classes registered, the OpenAPI will not contain any endpoints.");
        }
        
        Properties swaggerProps = getSwaggerProperties(propertiesLocation, bus);
        if (api.getInfo() == null) {
            api.setInfo(getInfo(swaggerProps));
        }

        registerOpenApiResources(sfb, api);
        registerSwaggerUiResources(sfb, swaggerProps, factory, bus);
    }

    public boolean isScan() {
        return scan;
    }

    public void setScan(boolean scan) {
        this.scan = scan;
    }

    public String getFilterClass() {
        return filterClass;
    }

    public void setFilterClass(String filterClass) {
        this.filterClass = filterClass;
    }
    
    public Set<String> getResourcePackages() {
        return resourcePackages;
    }
    
    public void setResourcePackages(Set<String> resourcePackages) {
        this.resourcePackages = (resourcePackages == null) ? null : new HashSet<>(resourcePackages);
    }

    public String getVersion() {
        return version;
    }

    public void setVersion(String version) {
        this.version = version;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public String getContactName() {
        return contactName;
    }

    public void setContactName(String contactName) {
        this.contactName = contactName;
    }

    public String getContactEmail() {
        return contactEmail;
    }

    public void setContactEmail(String contactEmail) {
        this.contactEmail = contactEmail;
    }

    public String getContactUrl() {
        return contactUrl;
    }

    public void setContactUrl(String contactUrl) {
        this.contactUrl = contactUrl;
    }

    public String getLicense() {
        return license;
    }

    public void setLicense(String license) {
        this.license = license;
    }

    public String getLicenseUrl() {
        return licenseUrl;
    }

    public void setLicenseUrl(String licenseUrl) {
        this.licenseUrl = licenseUrl;
    }

    public String getTermsOfServiceUrl() {
        return termsOfServiceUrl;
    }

    public void setTermsOfServiceUrl(String termsOfServiceUrl) {
        this.termsOfServiceUrl = termsOfServiceUrl;
    }

    public boolean isReadAllResources() {
        return readAllResources;
    }

    public void setReadAllResources(boolean readAllResources) {
        this.readAllResources = readAllResources;
    }

    public Set<String> getResourceClasses() {
        return resourceClasses;
    }

    public void setResourceClasses(Set<String> resourceClasses) {
        this.resourceClasses = (resourceClasses == null) ? null : new HashSet<>(resourceClasses);
    }

    public Collection<String> getIgnoredRoutes() {
        return ignoredRoutes;
    }

    public void setIgnoredRoutes(Collection<String> ignoredRoutes) {
        this.ignoredRoutes = (ignoredRoutes == null) ? null : new HashSet<>(ignoredRoutes);
    }

    public boolean isPrettyPrint() {
        return prettyPrint;
    }

    public void setPrettyPrint(boolean prettyPrint) {
        this.prettyPrint = prettyPrint;
    }
    
    public boolean isRunAsFilter() {
        return runAsFilter;
    }
    
    @Override
    public Boolean isSupportSwaggerUi() {
        return supportSwaggerUi;
    }

    public void setSupportSwaggerUi(Boolean supportSwaggerUi) {
        this.supportSwaggerUi = supportSwaggerUi;
    }

    public String getSwaggerUiVersion() {
        return swaggerUiVersion;
    }

    public void setSwaggerUiVersion(String swaggerUiVersion) {
        this.swaggerUiVersion = swaggerUiVersion;
    }

    public String getSwaggerUiMavenGroupAndArtifact() {
        return swaggerUiMavenGroupAndArtifact;
    }

    public void setSwaggerUiMavenGroupAndArtifact(
            String swaggerUiMavenGroupAndArtifact) {
        this.swaggerUiMavenGroupAndArtifact = swaggerUiMavenGroupAndArtifact;
    }

    @Override
    public Map<String, String> getSwaggerUiMediaTypes() {
        return swaggerUiMediaTypes;
    }

    public void setSwaggerUiMediaTypes(Map<String, String> swaggerUiMediaTypes) {
        this.swaggerUiMediaTypes = swaggerUiMediaTypes;
    }

    public String getConfigLocation() {
        return configLocation;
    }

    public void setConfigLocation(String configLocation) {
        this.configLocation = configLocation;
    }

    public String getPropertiesLocation() {
        return propertiesLocation;
    }

    public void setPropertiesLocation(String propertiesLocation) {
        this.propertiesLocation = propertiesLocation;
    }

    public void setRunAsFilter(boolean runAsFilter) {
        this.runAsFilter = runAsFilter;
    }

    public void setScanKnownConfigLocations(boolean scanKnownConfigLocations) {
        this.scanKnownConfigLocations = scanKnownConfigLocations;
    }
    
    public boolean isScanKnownConfigLocations() {
        return scanKnownConfigLocations;
    }
    
    public void setSwaggerUiConfig(final SwaggerUiConfig swaggerUiConfig) {
        this.swaggerUiConfig = swaggerUiConfig;
    }
    
    @Override
    public SwaggerUiConfig getSwaggerUiConfig() {
        return swaggerUiConfig;
    }

    @Override
    public String findSwaggerUiRoot() {
        return SwaggerUi.findSwaggerUiRoot(swaggerUiMavenGroupAndArtifact, swaggerUiVersion);
    }
    
    protected Properties getUserProperties(final Map<String, Object> userDefinedOptions) {
        final Properties properties = new Properties();
        
        if (userDefinedOptions != null) {
            userDefinedOptions
                .entrySet()
                .stream()
                .filter(entry -> entry.getValue() != null)
                .forEach(entry -> properties.setProperty(entry.getKey(), entry.getValue().toString()));
        }
        
        return properties;
    }

    protected void registerOpenApiResources(
            final JAXRSServiceFactoryBean sfb,
            final OpenAPI openApiDefinition) {

        sfb.setResourceClassesFromBeans(Collections.singletonList(new OpenApiEndpoint(openApiDefinition)));
    }

    protected void registerSwaggerUiResources(JAXRSServiceFactoryBean sfb, Properties properties,
            ServerProviderFactory factory, Bus bus) {
        
        final Registration swaggerUiRegistration = getSwaggerUi(bus, properties, isRunAsFilter());
        
        if (!isRunAsFilter()) {
            sfb.setResourceClassesFromBeans(swaggerUiRegistration.getResources());
        } 

        factory.setUserProviders(swaggerUiRegistration.getProviders());
    }

    /**
     * The info will be used only if there is no @OpenAPIDefinition annotation is present.
     */
    private org.eclipse.microprofile.openapi.models.info.Info getInfo(final Properties properties) {
        org.eclipse.microprofile.openapi.models.info.Info info = new InfoImpl()
            .title(getOrFallback(getTitle(), properties, TITLE_PROPERTY))
            .version(getOrFallback(getVersion(), properties, VERSION_PROPERTY))
            .description(getOrFallback(getDescription(), properties, DESCRIPTION_PROPERTY))
            .termsOfService(getOrFallback(getTermsOfServiceUrl(), properties, TERMS_URL_PROPERTY))
            .contact(new ContactImpl()
                .name(getOrFallback(getContactName(), properties, CONTACT_PROPERTY))
                .email(getContactEmail())
                .url(getContactUrl()));

        String licenseName = getOrFallback(getLicense(), properties, LICENSE_PROPERTY);
        if (licenseName != null) {
            info = info.license(new LicenseImpl()
                    .name(getOrFallback(getLicense(), properties, LICENSE_PROPERTY))
                    .url(getOrFallback(getLicenseUrl(), properties, LICENSE_URL_PROPERTY)));
        }
        return info;
    }

    private String getOrFallback(String value, Properties properties, String property) {
        if (value == null && properties != null) {
            return properties.getProperty(property);
        } else {
            return value;
        }
    }

    private Collection<String> scanResourcePackages(JAXRSServiceFactoryBean sfb) {
        return sfb
            .getClassResourceInfo()
            .stream()
            .map(cri -> cri.getServiceClass().getPackage().getName())
            .collect(Collectors.toSet());
    }
    
    private static boolean filterByPackage(final Class<?> cls, final Set<String> packages) {
        return (packages == null || packages.isEmpty()) 
            || packages.stream().anyMatch(pkg -> cls.getPackage().getName().startsWith(pkg));
    }
    
    private static boolean filterByClassName(final Class<?> cls, final Set<String> classes) {
        return (classes == null || classes.isEmpty()) 
            || classes.stream().anyMatch(cls.getName()::equalsIgnoreCase);
    }
}
